/**
 * Created by user on 2016-07-04.
 */
import {pick} from 'lodash'
import * as mongoose from 'mongoose'
import * as logger from 'winston'
import GameRecord from '../../database/models/gameRecord'
import {PlayerModel} from '../../database/models/player'
import DissolveRecord from '../../database/models/dissolveRecord'
import RoomRecord from '../../database/models/roomRecord'
import Club from '../../database/models/club'
import ClubMember from '../../database/models/clubMember'
import '../../utils/algorithm'
import {GameTypes} from "../gameTypes"
import {once} from "../onceDecorator"
import Game from './game'
import {eqlModelId} from "./modelId"
import {Channel} from 'amqplib'
import {autoSerialize, autoSerializePropertyKeys, serialize} from "../serializeDecorator"
import {PlayerRmqProxy} from "../../player/PlayerRmqProxy"
import Rule from "./Rule";
import Table from "./table"
import NormalTable from "./normalTable"
import {RoomBase} from "../IRoom"

const ObjectId = mongoose.Types.ObjectId

export const clubOwnerIsRich = async (clubId: string, rule, Room) => {
  if (!clubId) {
    return false
  }

  const club = await Club.findOne({_id: clubId}).populate('owner')
  if (!club || !club.owner) {
    return false
  }
  const fee = Room.roomFee(rule)

  return club.owner.gem >= fee;

}

export const playerInClub = async (clubShortId: string, playerId: string, gameType: string) => {
  if (!clubShortId) {
    return false
  }
  const club = await Club.findOne({shortId: clubShortId, gameType: gameType})
  if (!club) {
    return false
  }

  if (club.owner === playerId) {
    return true;
  }

  return ClubMember.findOne({club: club._id, member: playerId}).exec()
}

const gameType: GameTypes = "biaofen"

class Room extends RoomBase {
  @autoSerialize
  dissolveTime: number;

  @autoSerialize
  juIndex: number;

  @autoSerialize
  restJuShu: number;

  dissolveTimeout: NodeJS.Timer

  @serialize
  game: Game

  capacity: number

  @autoSerialize
  players: any[]

  @autoSerialize
  isPublic: boolean

  @autoSerialize
  snapshot: any[]
  disconnectCallback: (player) => void

  @autoSerialize
  readyPlayers: string[]

  @serialize
  playersOrder: any[]

  @autoSerialize
  charged: boolean
  charge: () => void

  @autoSerialize
  roomState: string = ''

  @serialize
  gameState: Table

  @autoSerialize
  scoreMap: any

  @autoSerialize
  disconnected: any[]
  zhuangCounter: number
  counterMap: any

  @autoSerialize
  ownerId: string

  @autoSerialize
  creator: any

  @autoSerialize
  creatorName: string

  currentBase: number

  @autoSerialize
  gameRule: any
  // noinspection TsLint
  @autoSerialize
  _id: string

  @autoSerialize
  uid: string
  nextStarterIndex: number = 0

  @autoSerialize
  dissolveReqInfo: Array<{ name: string, _id: string, type: string }> = []

  listenOn: string[]
  isPlayAgain: boolean = false

  @autoSerialize
  clubId: number = 0

  @autoSerialize
  clubMode: boolean = false

  @autoSerialize
  clubOwner: any

  autoDissolveTimer: NodeJS.Timer

  constructor(rule: any) {
    super()
    this.uid = ObjectId().toString()
    this.game = new Game(rule)
    this.isPublic = rule.isPublic
    this.gameRule = rule

    this.initPlayers()

    this.scoreMap = {}
    this.counterMap = {}
    this.gameState = null
    this.dissolveReqInfo = []
    this.charged = false
    this.charge = rule.share ? this.chargeAllPlayers.bind(this) : this.chargeCreator.bind(this)

    this.restJuShu = rule.juShu
    this.juIndex = 0
    this.autoDissolve();
  }

  autoDissolve() {
    this.autoDissolveTimer = setTimeout(() => {
      if (this.game.juIndex === 0 && !this.gameState) {
        this.autoDissolveFunc()
      }
    }, 30 * 60 * 1000);
  }

  async autoDissolveFunc() {
    await this.refundClubOwner();

    this.dissolveAndDestroyTable()
    this.players.forEach(player => {
      if (player) {
        player.sendMessage('room/dissolve', {})
        player.room = null
      }
    })
    this.emit('empty', this.disconnected.map(x => x[0]))
    this.players.fill(null)
    return true
  }

  static async recover(json: any, repository: { channel: Channel, userCenter: any }): Promise<Room> {
    const room = new Room(json.gameRule)
    // Object.assign(room.game.rule.ro, json.game.rule.ro)
    //
    const gameAutoKeys = autoSerializePropertyKeys(room.game)
    Object.assign(room.game, pick(json.game, gameAutoKeys))

    const keys = autoSerializePropertyKeys(room)
    Object.assign(room, pick(json, keys))


    for (const [index, playerId] of json.playersOrder.entries()) {
      if (playerId) {
        const model = await repository.userCenter.getPlayerModel(playerId)

        const playerRmq = new PlayerRmqProxy(
          model, repository.channel,
          gameType
        )

        if (json.players[index]) {
          room.players[index] = playerRmq
        }
        room.playersOrder[index] = playerRmq;
      }
    }

    for (const [index, playerId] of json.snapshot.entries()) {
      const model = await repository.userCenter.getPlayerModel(playerId)

      room.snapshot[index] = new PlayerRmqProxy(
        model, repository.channel, gameType
      )
    }

    if (room.clubMode) {
      const ownerModel = await repository.userCenter.getPlayerModel(room.clubOwner)

      room.clubOwner = new PlayerRmqProxy(
        ownerModel, repository.channel, gameType
      )
    }

    const creatorModel = await repository.userCenter.getPlayerModel(room.creator)
    if (creatorModel)
      room.creator = new PlayerRmqProxy(creatorModel, repository.channel, gameType)
    else {
      room.creator = {model: {_id: 'tournament'}}
      room.charge = () => {
      }
    }

    if (json.gameState) {
      room.gameState = new NormalTable(room, room.rule, room.game.juShu)
      room.gameState.resume(json)
    }

    if (room.roomState === 'dissolve') {
      let delayTime = room.dissolveTime + 180 * 1000 - Date.now();
      room.dissolveTimeout = setTimeout(() => {
        room.forceDissolve()
      }, delayTime)
    }

    return room
  }

  static roomFee(rule): number {

    const creatorFeeMap = {
      6: 2, 12: 4, 18: 6
    }

    const shareFeeMap = {
      6: 1, 12: 2, 18: 3
    }

    const juShu = rule.juShu
    if (rule.share) {
      return shareFeeMap[juShu] || 2
    }

    return creatorFeeMap[juShu] || 6
  }

  initPlayers() {
    this.snapshot = []
    this.readyPlayers = []
    this.disconnected = []
    this.capacity = 2 // fixme this.rule.playerCount || 4
    this.players = new Array(this.capacity).fill(null)
    this.playersOrder = new Array(this.capacity).fill(null)
    this.disconnectCallback = messageBoyd => {

      const disconnectPlayer = this.getPlayerById(messageBoyd.from)
      this.playerDisconnect(disconnectPlayer)
    }
  }

  initScore(player) {
    if (this.scoreMap[player._id] === undefined) {
      this.scoreMap[player._id] = 0
    }
  }

  clearScore(playerId) {
    if (!this.isPublic) {
      delete this.scoreMap[playerId];
    }
  }

  recordGameRecord(states, events) {

    const room = this.uid
    const players = states.map(state => state.model._id)
    const playersInfo = states.map(player => ({
      model: pick(player.model, ['name', 'headImgUrl', 'sex', 'gold', 'shortId'])
    }))
    const playerArray = states.map(state => ({
      name: state.model.name,
      headImgUrl: state.model.headImgUrl,
      score: state.score,
    }))

    GameRecord.create({
        room,
        players,
        playersInfo,
        record: playerArray,
        game: {roomId: this._id, rule: this.rule.getOriginData()},
        states,
        events,
        type: 'doudizhu'
      },
      err => {
        if (err) {
          logger.error(err)
        }
      }
    )
  }

  async recordRoomScore() {
    const players = this.snapshot.map(p => p._id)

    const scores = this.snapshot.map(player => ({
      score: this.scoreMap[player.model._id] || 0,
      name: player.model.name,
      headImgUrl: player.model.headImgUrl,
      shortId: player.model.shortId
    }))


    const roomRecord = {
      players,
      scores,
      roomNum: this._id,
      room: this.uid,
      creatorId: this.creator.model.shortId || 0,
      createAt: Date.now(),
      club: null,
      category: 'doudizhu',
      rule: this.rule.getOriginData()
    }

    if (this.clubId) {
      roomRecord.club = this.clubId;
    }

    try {
      await RoomRecord.update({room: this.uid}, roomRecord, {
        upsert: true,
        setDefaultsOnInsert: true
      }).exec()
    } catch (e) {
      logger.error(`${__filename}:261 recordRoomScore`, e)
    }
    return roomRecord
  }

  async recordDrawGameScore() {
    DissolveRecord.create({
        roomNum: this._id,
        juIndex: this.game.juIndex,
        category: 'biaofen',
        dissolveReqInfo: this.dissolveReqInfo,
      },
      err => {
        if (err) {
          logger.error(err)
        }
      }
    )
    if (this.gameState) {
      await this.recordRoomScore()
      this.recordGameRecord(this.gameState.getTableState(), this.gameState.recorder.getEvents())
    }
  }

  addScore(player, v) {
    switch (typeof player) {
      case 'string':
        const gains = v
        this.scoreMap[player] += gains

        break
      case 'object':
        player.addGold(v)
        this.scoreMap[player._id] = ((player && player.gold) || 0) - v
        break
      default:
        break
    }
  }

  removeDisconnected(item) {
    for (let i = 0; i < this.disconnected.length; i++) {
      if (this.disconnected[i] === item) {
        this.disconnected.splice(i, 1)
      }
    }
  }

  reconnect(reconnectPlayer) {
    const disconnectedItem = this.disconnected.find(x => eqlModelId(x[0], reconnectPlayer._id))
    // if (disconnectedItem) {
    // const [_, index] = disconnectedItem
    reconnectPlayer.room = this

    this.listen(reconnectPlayer)
    this.arrangePos(reconnectPlayer, true)
    this.mergeOrder()

    reconnectPlayer.on('disconnect', this.disconnectCallback)
    disconnectedItem && this.removeDisconnected(disconnectedItem)

    if (!this.gameState) {
      this.announcePlayerJoin(reconnectPlayer)
    }

    const i = this.snapshot.findIndex(p => p._id === reconnectPlayer._id)
    this.emit('reconnect', reconnectPlayer, i)
    this.broadcastRejoin(reconnectPlayer)
    if (this.dissolveTimeout) {
      this.updateReconnectPlayerDissolveInfoAndBroadcast(reconnectPlayer);
    }
    return true
    // }
  }

  inRoom(socket) {
    return this.players.indexOf(socket) > -1
  }

  joinMessageFor(newJoinPlayer): any {
    return {
      index: this.indexOf(newJoinPlayer),
      model: newJoinPlayer.model,
      ip: newJoinPlayer.getIpAddress(),
      location: newJoinPlayer.location,
      owner: this.ownerId,
      score: this.getScore(newJoinPlayer),
      base: this.currentBase,
      zhuangCounter: this.zhuangCounter,
      juIndex: this.game.juIndex,
      readyPlayers: this.readyPlayers.map(playerId => {
        const readyPlayer = this.inRoomPlayers.find(p => p._id === playerId)
        return this.players.indexOf(readyPlayer)
      }),
      disconnectedPlayers: this.disconnected.map(item => this.indexOf({_id: item[0]})),
    }
  }

  join(newJoinPlayer) {

    const isReconnect = this.indexOf(newJoinPlayer) >= 0
    if (isReconnect || this.disconnected.find(x => x[0] === newJoinPlayer._id)) {
      return this.reconnect(newJoinPlayer)
    }

    if (!this.canJoin(newJoinPlayer)) {
      return false
    }
    newJoinPlayer.room = this
    this.listen(newJoinPlayer)
    this.arrangePos(newJoinPlayer, false)

    this.mergeOrder()

    this.initScore(newJoinPlayer)

    this.emit('join')
    this.announcePlayerJoin(newJoinPlayer)

    this.pushToSnapshot(newJoinPlayer)
    return true
  }

  difen() {
    return this.game.rule.ro.difen
  }

  nextGame(thePlayer) {
    if (!this.isPublic && this.game.juShu <= 0) {
      thePlayer.sendMessage('room/join-fail', {reason: '牌局已经结束.'})
      return
    }
    if (this.indexOf(thePlayer) < 0) {
      thePlayer.sendMessage('room/join-fail', {reason: '你已经不属于这个房间.'})
      return false
    }

    this.announcePlayerJoin(thePlayer)
    // this.evictFromOldTable(thePlayer)

    return true
  }

  onRequestDissolve(player) {
    const dissolveInfo = this.getDissolvePlayerInfo(player);
    this.broadcast('room/dissolveReq', {dissolveReqInfo: dissolveInfo, startTime: this.dissolveTime});
    if (this.canDissolve()) {
      this.forceDissolve()
      return
    }

    if (!this.dissolveTimeout) {
      this.roomState = 'dissolve'
      this.dissolveTimeout = setTimeout(() => {
        this.forceDissolve()
      }, 180 * 1000)
    }

    return true;
  }

  onAgreeDissolve(player) {
    if (this.roomState !== 'dissolve') {
      return
    }

    const item = this.dissolveReqInfo.find((x) => {
      return x.name === player.model.name;
    });
    if (item) {
      item.type = 'agree';
    }
    this.broadcast('room/dissolveReq', {dissolveReqInfo: this.dissolveReqInfo});

    if (this.canDissolve()) {
      this.forceDissolve()
      return
    }
    return true;
  }

  onDisagreeDissolve(player) {
    if (this.roomState !== 'dissolve') {
      return
    }

    const item = this.dissolveReqInfo.find((x) => {
      return x.name === player.model.name;
    });
    if (item) {
      item.type = 'disAgree';
      clearTimeout(this.dissolveTimeout)
      this.roomState = ''
      this.dissolveTimeout = null
    }
    this.broadcast('room/dissolveReq', {dissolveReqInfo: this.dissolveReqInfo});
    return true;
  }


  evictFromOldTable(thePlayer) {
    const oldTable = this.gameState
    oldTable.evictPlayer(thePlayer)
  }

  getDissolvePlayerInfo(player) {
    this.dissolveReqInfo = [];
    this.dissolveTime = Date.now();
    this.dissolveReqInfo.push({
      type: 'originator',
      name: player.model.name,
      _id: player.model._id
    });
    for (let i = 0; i < this.players.length; i++) {
      const pp = this.players[i];
      if (pp && pp.isRobot()) {
        this.dissolveReqInfo.push({
          type: 'agree',
          name: pp.model.name,
          _id: pp.model._id
        });
      } else if (pp && pp !== player) {
        this.dissolveReqInfo.push({
          type: 'waitConfirm',
          name: pp.model.name,
          _id: pp.model._id
        });
      }
    }
    for (let i = 0; i < this.disconnected.length; i++) {
      const pp = this.disconnected[i];
      this.snapshot.forEach(player => {
          if (pp && player.model._id === pp[0]) {
            this.dissolveReqInfo.push({
              type: 'offline',
              name: player.model.name,
              _id: player.model._id
            });
          }
        }
      )
    }
    return this.dissolveReqInfo;
  }

  updateReconnectPlayerDissolveInfoAndBroadcast(reconnectPlayer) {
    const item = this.dissolveReqInfo.find((x) => {
      return x.name === reconnectPlayer.model.name;
    });
    if (item) {
      if (item.type === 'agree_offline') {
        item.type = 'agree';
      } else if (item.type !== 'originator') {
        item.type = 'waitConfirm';
      }
    }
    this.broadcast('room/dissolveReq',
      {dissolveReqInfo: this.dissolveReqInfo, startTime: this.dissolveTime});
  }

  updateDisconnectPlayerDissolveInfoAndBroadcast(player) {
    const item = this.dissolveReqInfo.find((x) => {
      return x.name === player.model.name;
    });
    if (item) {
      if (item.type === 'agree') {
        item.type = 'agree_offline'
      } else if (item.type !== 'originator') {
        item.type = 'offline';
      }
    }
    this.broadcast('room/dissolveReq', {dissolveReqInfo: this.dissolveReqInfo, startTime: this.dissolveTime});
  }

  //todo delete?
  canDissolve() {
    if (this.dissolveReqInfo.length === 0) {
      return false
    }

    const onLinePlayer = this.dissolveReqInfo
      .filter(reqInfo => {
        const id = reqInfo._id
        return !this.disconnected.some(item => item[0] === id)
      })
    const agreeReqs = this.dissolveReqInfo.filter(reqInfo => reqInfo.type === 'agree'
      || reqInfo.type === 'originator' || reqInfo.type === 'agree_offline')

    return agreeReqs.length > 0 && agreeReqs.length + 1 >= this.dissolveReqInfo.length

  }

  playerDisconnect(player) {
    const p = player
    const index = this.players.indexOf(player)

    if (index === -1) {
      return
    }

    if (this.isPublic && !this.gameState) {
      this.leave(player)
      return
    }

    p.room = null
    if (!this.gameState) {
      this.cancelReady(p._id)
    }

    if (this.dissolveTimeout) {
      this.updateDisconnectPlayerDissolveInfoAndBroadcast(player);
    }

    this.broadcast('room/playerDisconnect', {index: this.players.indexOf(player)}, player.msgDispatcher)
    this.removePlayer(player)
    this.disconnected.push([player._id, index])
    this.emit('disconnect', p._id)
  }

  //todo should delete?
  leave(player) {
    if (!player) return false
    if (this.game.juIndex > 0 && !this.isRoomAllOver()) return false

    this.removePlayer(player)
    this.removeOrder(player)

    player.room = null

    this.broadcast('room/leave', {_id: player._id})
    this.cancelReady(player._id)

    this.emit('leave', {_id: player._id})
    if (this.isEmpty() && this.isPublic) {
      this.emit('empty', this.disconnected);
      this.readyPlayers = [];
    }
    this.broadcast('room/leave', {_id: player._id});
    this.removeReadyPlayer(player._id);
    return true
  }

  //todo should delete?
  ready(player) {
    if (this.isReadyPlayer(player._id)) {
      return
    }

    if (this.gameState) {
      return
    }

    this.readyPlayers.push(player._id)
    this.broadcast('room/playerReady', {
      index: this.players.indexOf(player),
      readyPlayers: this.readyPlayers
    })

    if (this.allReady) {
      //需等1203测完改的bug========================
      if (!this.isRoomAllOver() || true) {
        this.playersOrder = this.players.slice()
        this.clearReady()
        this.startGame()
      }

    }
  }

  //todo should delete?
  get allReady() {
    return this.readyPlayers.length === this.capacity
  }

  private sortPlayer(nextStarterIndex: number) {
    if (nextStarterIndex === 0) {
      return
    }

    const playersCopy = new Array(this.players.length);
    const newOrders = new Array(this.players.length);

    for (let i = 0; i < playersCopy.length; i++) {
      let from = (nextStarterIndex + i) % playersCopy.length;
      playersCopy[i] = this.players[from];
      newOrders[i] = this.playersOrder[from];
    }
    this.players = playersCopy;
    this.playersOrder = newOrders
  }

  isRoomAllOver(): boolean {
    return this.game.isAllOver()
  }


  async gameOver(states, firstPlayerId) {

    states.forEach(state => {
      state.model.played += 1
      this.addScore(state.model._id, state.score)
    })

    this.clearReady()
    await this.recordRoomScore()
    this.recordGameRecord(states, this.gameState.recorder.getEvents())

    this.gameState.destroy()
    this.gameState = null

    if (this.isRoomAllOver()) {
      const message = this.allOverMessage()
      this.broadcast('room/allOver', message);
      try {

        for (const [i, p] of this.playersOrder.entries()) {
          await p.addGold(states[i].prize)
        }
      } catch (e) {
        logger.error({scope: 'addGold', error: e.message, stack: e.stack})
      }

      this.players.forEach(x => x && this.leave(x));
      this.emit('empty', this.disconnected);

    }
  }

  allOverMessage() {
    const message = {players: {}, roomNum: this._id, juShu: this.game.juIndex, isClubRoom: this.clubMode}
    this.snapshot
      .filter(p => p)
      .forEach(player => {
        message.players[player.model._id] = {
          userName: player.model.name,
          headImgUrl: player.model.headImgUrl,
          gold: player.model.gold
        }
      })

    Object.keys(this.counterMap).forEach(eventKey => {
      this.counterMap[eventKey].forEach(p => {
        message.players[p][eventKey] = (message.players[p][eventKey] || 0) + 1
      })
    })
    this.snapshot.forEach(p => {
      const playerId = p._id
      message.players[playerId] && (message.players[playerId].score = this.scoreMap[playerId])
    })


    const creator = message.players[this.creator.model._id]
    if (creator) {
      creator['isCreator'] = true
    }
    return message
  }

  get rule() {
    const {ro} = this.game.rule
    const {quizzes, ...reset} = ro
    return new Rule(reset)
  }

  privateRoomFee() {
    return Room.roomFee(this.game.rule)
  }

  @once
  async refundClubOwner() {
    if (!this.clubMode) return
    if (this.charged) return
    if (this.gameRule.clubPersonalRoom) return

    const fee = Room.roomFee(this.rule)

    PlayerModel.update({_id: this.clubOwner._id},
      {
        $inc: {
          gem: fee,
        },
      },
      (err) => {
        if (err) {
          logger.error(this.clubOwner, err)
        }
      })

    this.clubOwner.sendMessage('resource/createRoomUsedGem', {
      createRoomNeed: -fee
    })
  }

  applyAgain(player) {
    if (player !== this.creator) {
      player.sendMessage('room/againReply', {ok: false, info: '不是房主'})
      return
    }

    if (!this.enoughCurrency(player)) {
      player.sendMessage('room/againReply', {ok: false, info: '余额不足'})
      return
    }

    this.playAgain()
  }

  enoughCurrency(player) {
    return player.model.gem >= this.privateRoomFee()
  }

  playAgain() {
    this.isPlayAgain = true

    this.game.reset()
    this.clearReady()
    this.noticeAnother()
  }

  noticeAnother() {
    const excludeCreator = this.inRoomPlayers.filter(s => s !== this.creator)
    excludeCreator.forEach(pSocket => pSocket.sendMessage('room/inviteAgain', {}))
  }

  playerOnExit(player) {
    this.leave(player)
    this.removeRoomListeners(player)
  }

  listen(player) {
    this.listenOn = ['room/again', 'room/exit', 'disconnect']

    player.on('room/again', () => this.applyAgain(player))
    player.on('room/exit', () => this.playerOnExit(player))
    player.on('disconnect', this.disconnectCallback)
  }

  removeRoomListeners(player) {
    this.listenOn.forEach(name => player.socket.removeAllListeners(name))
  }
}

export default Room
